#!/bin/bash
# * =====================================================
# * Copyright © hk. 2022-2025. All rights reserved.
# * File name  : net-nfs.sh
# * Author     : 苏木
# * Date       : 2025-05-04
# * ======================================================
##

# 脚本和工程路径
# ========================================================
SCRIPT_NAME=${0#*/}
SCRIPT_CURRENT_PATH=${0%/*}
SCRIPT_ABSOLUTE_PATH=`cd $(dirname ${0}); pwd`
PROJECT_ROOT=${SCRIPT_ABSOLUTE_PATH} # 工程的源码目录，一定要和编译脚本是同一个目录
SOFTWARE_DIR_PATH=~/2software        # 软件安装目录
TFTP_DIR=~/3tftp
NFS_DIR=~/4nfs
CPUS=$(($(nproc)-1))                 # 使用总核心数-1来多线程编译

# 打开错误状态监测
# ========================================================
# -e 表示当脚本中的任何命令以 ‌非零状态（失败）‌ 退出时，立即终止脚本执行
# -E 确保 ERR 陷阱（trap）在 ‌函数、子 Shell 或命令替换‌ 中生效
set -eE

# 可用的emoji符号
# ========================================================
# https://www.w3school.com.cn/charsets/ref_emoji.asp
function usage_emoji()
{
    echo -e "⚠️ ✅ ❌ 🚩 📁 🕣️ 🔔"
}

# 颜色和日志标识
# ========================================================
# |      ---       |Black |  Red | Green | Yellow | Blue | Magenta | Cyan | White |
# | Fore(Standard) |  30  |  31  |  32   |   33   |  34  |   35    |  35  |   37  |
# | Fore(light)    |  90  |  91  |  92   |   93   |  94  |   95    |  95  |   97  |
# | Back(Standard) |  40  |  41  |  42   |   43   |  44  |   45    |  46  |   47  |
# | Back(light)    | 100  | 101  | 102   |  103   | 104  |  105    | 106  |  107  |
BLACK="\033[1;30m"
RED='\033[1;31m'    # 红
GREEN='\033[1;32m'  # 绿
YELLOW='\033[1;33m' # 黄
BLUE='\033[1;34m'   # 蓝
PINK='\033[1;35m'   # 紫
CYAN='\033[1;36m'   # 青
WHITE='\033[1;37m'  # 白
CLS='\033[0m'       # 清除颜色

INFO="${GREEN}INFO: ${CLS}"
WARN="${YELLOW}WARN: ${CLS}"
ERROR="${RED}ERROR: ${CLS}"

# 日志打印
# ========================================================
# Export global functions
set -a # 自动导出，相当于export

function sh_log()
{
	LOG_COLOR="$1"           # 获取要显示的前景色
	shift                    # 参数左移
	if [ "$1" = "-n" ]; then # -n表示取消echo的换行
		shift
		LOG_FLAG="-ne"
	else
		LOG_FLAG="-e"
	fi
	echo $LOG_FLAG "\e[${LOG_COLOR}m$@\e[0m"

}

function prt()
{
	sh_log 0 "$@" # default
}

function message()
{
	sh_log 36 "$@" # light blue
}

function notice()
{
    echo -n "✨ "
	sh_log 35 "$@" # purple
}

function warning()
{
    echo -n "⚠️  "
	sh_log 33 "$@" # dark yellow
}

function error()
{
    echo -n "🔖 "
	sh_log 91 "$@" # light red
}

function success()
{
    echo -n "✅ "
	sh_log 32 "$@" # light red
}

function fatal()
{
    echo -n "🔖 "
	sh_log 31 "$@" # dark red
}

function info()
{
    echo -ne "${INFO}"
	sh_log 0 "$@" # dark red
}

function log_echo_demo()
{
    info "log help:"
    prt -n "Log colors:|"
	message -n " message |"
    success -n " success |"
	notice -n " notice |"
	warning -n " warning |"
	error -n " error |"
	fatal " fatal |"
	echo
}
set +a
# End of global functions

# 调试信息
# ========================================================
HOOK_RET=0    # 存储命令的状态
trap 'err_handler' ERR # trap 是用于信号处理的内置命令，可捕获系统信号或脚本事件并执行指定操作，常用于实现资源清理、中断处理和调试功能
                       #  EXIT	0	脚本正常退出时触发
                       #  ERR	-	命令返回非零状态码时触发，但是若命令返回值被捕获的话就不会触发
                       #            例如 false || exit_code=1
function err_handler()
{
	ret=${1:-$?}
	if [ "$ret" -eq 0 ]; then
		return 0
	fi

	fatal "ERROR: Running $BASH_SOURCE - ${2:-${FUNCNAME[1]}} failed!"
	fatal "ERROR: exit code $ret from line ${BASH_LINENO[0]}:"
	fatal "    ${3:-$BASH_COMMAND}"
	fatal "ERROR: call stack:"
	for i in $(seq 1 $((${#FUNCNAME[@]} - 1))); do
		SOURCE="${BASH_SOURCE[$i]}"
		LINE=${BASH_LINENO[$(( $i - 1 ))]}
		fatal "    $(basename "$SOURCE"): ${FUNCNAME[$i]}($LINE)"
	done
	exit $ret
}

function err_handler_demo()
{
    false                  # 这种写法会直接触发trap注册的处理函数
    # HOOK_RET=${PIPESTATUS[0]}
	# if [ $HOOK_RET -ne 0 ]; then
	# 	err_handler $HOOK_RET "${FUNCNAME[0]} $*" "$@"
	# 	exit $HOOK_RET
	# fi
}

function err_handler_demo_test()
{
    err_handler_demo err_test_demo 1
}

# 时间计算
# ========================================================
TIME_START=
TIME_END=

function get_start_time()
{
	TIME_START=$(date +'%Y-%m-%d %H:%M:%S')
}

function get_end_time()
{
	TIME_END=$(date +'%Y-%m-%d %H:%M:%S')
}

function get_execute_time()
{
	start_seconds=$(date --date="$TIME_START" +%s);
	end_seconds=$(date --date="$TIME_END" +%s);
	duration=`echo $(($(date +%s -d "${TIME_END}") - $(date +%s -d "${TIME_START}"))) | awk '{t=split("60 s 60 m 24 h 999 d",a);for(n=1;n<t;n+=2){if($1==0)break;s=$1%a[n]a[n+1]s;$1=int($1/a[n])}print s}'`
	echo "===*** 🕣️ 运行时间：$((end_seconds-start_seconds))s,time diff: ${duration} ***==="
}

function time_count_down
{
    for i in {3..0}
    do     

        echo -ne "${INFO}after ${i} is end!!!"
        echo -ne "\r\r"        # echo -e 处理特殊字符  \r 光标移至行首，但不换行
        sleep 1
    done
    echo "" # 打印一个空行，防止出现混乱
}

function get_run_time_demo()
{
    get_start_time
    time_count_down
    get_end_time
    get_execute_time
}

# 目录切换函数定义
# ========================================================
function cdi()
{
    if command -v pushd &>/dev/null; then
        # 压栈并切换
        pushd $1 >/dev/null || return 1
    else
        cd $1
    fi
}

function cdo()
{
    if command -v popd &>/dev/null; then
        # 弹出并恢复
        popd >/dev/null || return 1
    else
        cd -
    fi
}

# 命令执行函数定义
# ========================================================
#execute执行语句成功与否打印,此命令不会触发,主要是用于一些需要sudo的命令，这里可以自动判断
function execute() 
{
    local PASSWD="000000"
    local execute_exit_code=0
    local cmd_str

    # local cmd_str_src
    # printf -v cmd_str_src "%q " "$@" # 构造安全的命令字符串,不转义特殊字符

    # 重组所有参数为命令字符串（保留用户输入的转义符号）
    cmd_str=$(printf "%s " "$@" | sed 's/ $//')
    cmd_str=${cmd_str% }  # 移除末尾空格

    # 打印命令（高亮提示）
    if [ "$EUID" -ne 0 ]; then
        printf "\033[36mcmd:\033[0m sudo %s\n" "$cmd_str"
    else
        printf "\033[36mcmd:\033[0m %s\n" "$cmd_str"
    fi

    # 执行命令（通过 bash -c 解析复杂命令）
    if [ "$EUID" -ne 0 ]; then
        echo "$PASSWD" | sudo -S bash -c "$cmd_str" || execute_exit_code=1
    else
        bash -c "$cmd_str" || execute_exit_code=1
    fi

    # 错误处理
    if [ $execute_exit_code -ne 0 ]; then
        error "执行 $cmd_str 失败!"
        return 1 # trap 仅对当前 Shell 进程有效，子进程继承独立的信号处理环境，父进程无法感知子进程的 exit 行为
                 # 这里用return可以保持进程统一性，确保 trap 生效
    fi
}

function execute_demo()
{
    
    # execute false # 用于测试出错后被tap捕获
    execute apt-get update # 加上soudo执行

    local str=sumu1
    execute echo "${str}" \> /etc/demo.txt
    info "demo.txt内容如下:"
    cat /etc/demo.txt
    echo

    str=sumu2
    execute echo "${str}" \| tee -a /etc/demo.txt
    info "demo.txt内容如下:"
    cat /etc/demo.txt
    echo

    str=sumu3
    execute echo "${str}" \>\> /etc/demo.txt
    info "demo.txt内容如下:"
    cat /etc/demo.txt
    echo
}

# 开发环境信息
# ========================================================
function get_ubuntu_info()
{
    local kernel_version=$(uname -r) # 获取内核版本信息，-a选项会获得更详细的版本信息
    local ubuntu_version=$(lsb_release -ds) # 获取Ubuntu版本信息

    
    local ubuntu_ram_total=$(cat /proc/meminfo |grep 'MemTotal' |awk -F : '{print $2}' |sed 's/^[ \t]*//g')   # 获取Ubuntu RAM大小
    local ubuntu_swap_total=$(cat /proc/meminfo |grep 'SwapTotal' |awk -F : '{print $2}' |sed 's/^[ \t]*//g') # 获取Ubuntu 交换空间swap大小
    #local ubuntu_disk=$(sudo fdisk -l |grep 'Disk' |awk -F , '{print $1}' | sed 's/Disk identifier.*//g' | sed '/^$/d') #显示硬盘，以及大小
    local ubuntu_cpu=$(grep 'model name' /proc/cpuinfo |uniq |awk -F : '{print $2}' |sed 's/^[ \t]*//g' |sed 's/ \+/ /g') #cpu型号
    local ubuntu_physical_id=$(grep 'physical id' /proc/cpuinfo |sort |uniq |wc -l) #物理cpu个数
    local ubuntu_cpu_cores=$(grep 'cpu cores' /proc/cpuinfo |uniq |awk -F : '{print $2}' |sed 's/^[ \t]*//g') #物理cpu内核数
    local ubuntu_processor=$(grep 'processor' /proc/cpuinfo |sort |uniq |wc -l) #逻辑cpu个数(线程数)
    local ubuntu_cpu_mode=$(getconf LONG_BIT) #查看CPU当前运行模式是64位还是32位

    # 打印结果
    prt "ubuntu: $ubuntu_version - $ubuntu_cpu_mode"
    prt "kernel: $kernel_version"
    prt "ram   : $ubuntu_ram_total"
    prt "swap  : $ubuntu_swap_total"
    prt "cpu   : $ubuntu_cpu,physical id is$ubuntu_physical_id,cores is $ubuntu_cpu_cores,processor is $ubuntu_processor"
}

# 本地虚拟机VMware开发环境信息
function get_dev_env_info()
{
    prt "Development environment: "
    prt "ubuntu : 20.04.2-64(1核12线程 16GB RAM,512GB SSD) arm"
    prt "VMware : VMware® Workstation 17 Pro 17.6.0 build-24238078"
    prt "Windows: "
    prt "          处理器 AMD Ryzen 7 5800H with Radeon Graphics 3.20 GHz 8核16线程"
    prt "          RAM	32.0 GB (31.9 GB 可用)"
    prt "          系统类型	64 位操作系统, 基于 x64 的处理器"
    prt "linux开发板原始系统组件版本:"
    prt "          uboot : v2019.04 https://github.com/nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0"
    prt "          kernel: v4.19.71 https://github.com/nxp-imx/linux-imx/releases/tag/v4.19.71"
    prt "          rootfs: buildroot-2023.05.1 https://buildroot.org/downloads/buildroot-2023.05.1.tar.gz"
    prt ""
    prt "x86_64-linux-gnu   : gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)"
    prt "arm-linux-gnueabihf:"
    prt "          arm-linux-gnueabihf-gcc 8.3.0"
    prt "          https://developer.arm.com/-/media/Files/downloads/gnu-a/8.3-2019.03/binrel/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf.tar.xz"
}

# 环境变量
# ========================================================
# Github Actions托管的linux服务器有以下用户级环境变量，系统级环境变量加上sudo好像也权限修改
# .bash_logout  当用户注销时，此文件将被读取，通常用于清理工作，如删除临时文件。
# .bashrc       此文件包含特定于 Bash Shell 的配置，如别名和函数。它在每次启动非登录 Shell 时被读取。
# .profile、.bash_profile 这两个文件位于用户的主目录下，用于设置特定用户的环境变量和启动程序。当用户登录时，
#                        根据 Shell 的类型和配置，这些文件中的一个或多个将被读取。
USER_ENV=(~/.bashrc ~/.profile ~/.bash_profile)
SYSENV=(/etc/profile) # 系统环境变量位置
ENV_FILE=("${USER_ENV[@]}" "${SYSENV[@]}")

function source_env_info()
{
    for temp in ${ENV_FILE[@]};
    do
        if [ -f ${temp} ]; then
            prt "${INFO}source ${temp}"
            source ${temp}
        fi
    done
}


# ========================================================
# 功能实现
# ========================================================
EXECUTE_MODE=manu
function usage()
{
	prt "================================================="
    prt "${PINK}./${SCRIPT_NAME}      : 根据菜单选择功能${CLS}"
    prt "${PINK}./${SCRIPT_NAME} -h   : 显示打印信息用法后显示菜单${CLS}"
    prt "${PINK}./${SCRIPT_NAME} -a   : 自动执行所有步骤${CLS}"
    prt "================================================="
}
# 脚本运行参数处理
notice "There are $# parameters: $@ (\$1~\$$#)"
while getopts "ahp:" arg #选项后面的冒号表示该选项需要参数
    do
        case ${arg} in
            a) EXECUTE_MODE="auto" ;;
            h)
                log_echo_demo
                ;;
            p)
                if [ $OPTARG == "1" ];then
                    message "OPTARG=$OPTARG"
                fi
                ;;
            ?)  #当有不认识的选项的时候arg为?
                error "unkonw argument..."
                exit 1
                ;;
        esac
    done

# 功能函数实现
# ========================================================
# 检查包安装状态函数
function check_install() 
{
    if dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q "ok installed"; then
        return 0
    else
        return 1
    fi
}

# 服务状态检查
function check_service_active() 
{
    systemctl is-active --quiet $1
}

# 卸载nfs-kernel-server
function nfs_uninstall()
{
    execute apt-get purge -y nfs-kernel-server
    execute apt-get purge -y nfs-common
}

# 安装nfs服务端
function nfs_server()
{
    local package=nfs-kernel-server
    # 服务端检测
    if ! check_install ${package}; then
        warning "未检测到${package},即将开始安装"
        execute apt-get update -qq
        # 解决报错：无法创建主目录"/var/lib/nfs"
        execute mkdir -p /var/lib/nfs
        execute chmod 755 /var/lib/nfs
        execute chown nobody:nogroup /var/lib/nfs
        execute apt-get install -y ${package}
    fi
    # 再次检测
    if check_install ${package}; then
        success "nfs服务端${package}已安装,版本如下:"
        # dpkg -l | grep -E "tftp-hpa"
        dpkg -s ${package} | awk -F: '$0 ~ "Version"'
    else
        error "nfs服务端不存在!"
    fi
}

function nfs_client()
{
    local package=nfs-common
    # 客户端检测
    if ! check_installed ${package}; then
        warning "未检测到${package},即将开始安装"
        execute apt-get update -qq
        execute apt-get install -y ${package}
    else
        success "NFS客户端${package}已安装!"
    fi
}

# nfs配置
function nfs_dir_config()
{
    cdi ${PROJECT_ROOT}/etc
    prt "${PINK}current path         :$(pwd)${CLS}"

    # 创建共享目录
    if [ ! -d "${NFS_DIR}" ]; then
        info "创建共享目录: ${NFS_DIR}"
        mkdir -p "${NFS_DIR}"
        chmod 777 "${NFS_DIR}"
        execute chown nobody:nogroup "${NFS_DIR}"
    fi

    # 配置exports文件
    local clients=${CLIENTS:-"*"} # 默认允许所有客户端
    local options=${OPTIONS:-"rw,sync,no_subtree_check,no_root_squash"} # 默认挂载选项
    local nfs_absolute_dir=$(readlink -f "${NFS_DIR}") 
    # 注意这里一定要是绝对路径，否则可能挂载失败
    # /home/sumu/4nfs *(rw,sync,no_subtree_check,no_root_squash)
    local export_entry="${nfs_absolute_dir} ${clients}(${options})"

    if ! grep -qFx "${export_entry}" /etc/exports; then
        info "添加配置到/etc/exports: ${export_entry}"
        execute echo "\"${export_entry}\"" \>\> /etc/exports
        # 应用配置
        execute exportfs -a
        success "NFS共享配置完成"
    else
        warning "该共享配置已存在，跳过重复配置!"
    fi
    # 显示最终共享配置
    info "当前生效的NFS共享"
    execute exportfs -v

    info "重启nfs服务:"
    execute systemctl restart nfs-kernel-server
    success "nfs配置完成"
    cdo
}

# 在一些新版本中nfs老的版本协议不支持，但是对于arm中的开发，例如uboot, kernel它们可能就只支持nfs2.0协议
# ubuntu不支持2.0协议的花，可能就无法实现挂载nfs目录
function nfs_info
{
    info "nfs支持的所有版本如下:"
    execute cat /proc/fs/nfsd/versions

    # showmount：依赖于nfs-commom
    info "NFS共享目录如下:"
    showmount -e localhost

    info "NFS共享目录实际挂载情况如下:"
    showmount -a # 当前已连接的所有 NFS 客户端及其挂载点‌，输出格式为 客户端IP:挂载目录

    # 查看当前生效配置
    info "NFS共享目录当前生效配置如下:"
    execute exportfs -v
}

function nfs_version_config()
{
    local sys_cfg_file=/etc/default/nfs-kernel-server
    local local_cfg_file=nfs-kernel-server.tmp

    # 将系统配置文件拷贝到当前目录创建临时修改文件，防止权限不足
    cp -pv ${sys_cfg_file} ${local_cfg_file}

    # 处理已存在--nfs-version的情况（添加2.0）
    sed -i -E '/^RPCNFSDOPTS=.*--nfs-version /s/(--nfs-version )([0-9,.]+)/\12,\2/' "$local_cfg_file"
    # 处理没有--nfs-version但存在RPCNFSDOPTS的情况
    sed -i -E '/^RPCNFSDOPTS=/ {/--nfs-version/! s/"(.*)"/"\1 --nfs-version 2,3,4,4.1,4.2"/}' "$local_cfg_file"

    # 处理完全不存在RPCNFSDOPTS行的情况
    if ! grep -q '^RPCNFSDOPTS=' "$local_cfg_file"; then
        echo 'RPCNFSDOPTS="--nfs-version 2,3,4,4.1,4.2"' >> "$local_cfg_file"
    fi

    # 覆盖原来系统的配置文件
    execute cp ${local_cfg_file} ${sys_cfg_file}

    # 重启NFS服务
    info "重启NFS服务..."
    execute systemctl restart nfs-kernel-server

    # 验证结果
    info "当前支持的NFS协议版本:"
    execute cat /proc/fs/nfsd/versions

    # 删除临时文件
    info "删除临时配置文件:${local_cfg_file}"
    rm -rf ${local_cfg_file}
    success "成功更新NFS协议配置!"
}

# nfs测试说明
function nfs_local_check()
{
	info "测试步骤如下:"
	prt "cd ~/4nfs                   # 进入nfs工作目录"
	prt "touch test.txt              # 创建测试文件"
	prt "cd ~                        # 回到家目录"
	prt "sudo mkdir -p /mnt/nfs_temp # 创建挂载目录"
	prt "sudo mount -t nfs localhost:${NFS_DIR} /mnt/nfs_temp # 挂载nfs" 
	prt "ls /mnt/nfs_temp             # 查看是否挂载成功"
	prt "sudo umount /mnt/nfs_temp    # 卸载挂载"
	prt "sudo rm -rf /mnt/nfs_temp    # 卸载挂载"
}

# 自动执行所有步骤
function auto_setup() 
{
    nfs_server
    nfs_client
    nfs_dir_config
    nfs_version_config
    nfs_info
    nfs_local_check
    success "所有步骤执行完成!"
}

# 菜单与主进程
# ========================================================
function show_menu()
{
    message "================================================="
	message "                script by @苏木    ${CLS}"
	message "================================================="
    prt "${PINK}current path         :$(pwd)${CLS}"
    prt "${PINK}PROJECT_ROOT         :${PROJECT_ROOT}${CLS}"
    echo ""
    prt "* [0] 安装nfs服务端和客户端"
    prt "* [1] nfs共享目录配置"
    prt "* [2] nfs2.0协议支持"
    prt "* [3] 查看当前系统nfs信息"
    prt "* [4] 本地主机测试方案"
    prt "* [5] 卸载nfs相关工具"
    echo "================================================="
}

# 主逻辑
function main() 
{
    if [ "$EXECUTE_MODE" = "manu" ]; then
        while true; do
            show_menu
            read -p "请选择操作 (0-5): " choice
            case "${choice}" in
                "0") 
                    nfs_server
                    nfs_client
                    ;;
                "1") nfs_dir_config;;
                "2") nfs_version_config;;
                "3") nfs_info;;
                "4") nfs_local_check;;
                "5") nfs_uninstall;;
                *) error "无效选项!!!" ;;
            esac
            read -p "按回车继续..."
        done
    else
        info "正在自动执行所有配置步骤..."
        auto_setup
    fi
}

# 执行入口
# show_menu # 菜单显示
main "$@" # 主逻辑执行
